Conditions | 1 |
Paths | 16384 |
Total Lines | 356 |
Lines | 0 |
Ratio | 0 % |
Changes | 3 | ||
Bugs | 0 | Features | 0 |
Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.
For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.
Commonly applied refactorings include:
If many parameters/temporary variables are present:
1 | /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
||
43 | var TextLayerBuilder = (function TextLayerBuilderClosure() { |
||
44 | function TextLayerBuilder(options) { |
||
45 | this.textLayerDiv = options.textLayerDiv; |
||
46 | this.renderingDone = false; |
||
47 | this.divContentDone = false; |
||
48 | this.pageIdx = options.pageIndex; |
||
49 | this.pageNumber = this.pageIdx + 1; |
||
50 | this.matches = []; |
||
51 | this.viewport = options.viewport; |
||
52 | this.textDivs = []; |
||
53 | this.findController = options.findController || null; |
||
54 | } |
||
55 | |||
56 | TextLayerBuilder.prototype = { |
||
57 | _finishRendering: function TextLayerBuilder_finishRendering() { |
||
58 | this.renderingDone = true; |
||
59 | |||
60 | var event = document.createEvent('CustomEvent'); |
||
61 | event.initCustomEvent('textlayerrendered', true, true, { |
||
62 | pageNumber: this.pageNumber |
||
63 | }); |
||
64 | this.textLayerDiv.dispatchEvent(event); |
||
65 | }, |
||
66 | |||
67 | renderLayer: function TextLayerBuilder_renderLayer() { |
||
68 | var textLayerFrag = document.createDocumentFragment(); |
||
69 | var textDivs = this.textDivs; |
||
70 | var textDivsLength = textDivs.length; |
||
71 | var canvas = document.createElement('canvas'); |
||
72 | var ctx = canvas.getContext('2d'); |
||
73 | |||
74 | // No point in rendering many divs as it would make the browser |
||
75 | // unusable even after the divs are rendered. |
||
76 | if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) { |
||
77 | this._finishRendering(); |
||
78 | return; |
||
79 | } |
||
80 | |||
81 | var lastFontSize; |
||
82 | var lastFontFamily; |
||
83 | for (var i = 0; i < textDivsLength; i++) { |
||
84 | var textDiv = textDivs[i]; |
||
85 | if (textDiv.dataset.isWhitespace !== undefined) { |
||
86 | continue; |
||
87 | } |
||
88 | |||
89 | var fontSize = textDiv.style.fontSize; |
||
90 | var fontFamily = textDiv.style.fontFamily; |
||
91 | |||
92 | // Only build font string and set to context if different from last. |
||
93 | if (fontSize !== lastFontSize || fontFamily !== lastFontFamily) { |
||
94 | ctx.font = fontSize + ' ' + fontFamily; |
||
95 | lastFontSize = fontSize; |
||
96 | lastFontFamily = fontFamily; |
||
97 | } |
||
98 | |||
99 | var width = ctx.measureText(textDiv.textContent).width; |
||
100 | if (width > 0) { |
||
101 | textLayerFrag.appendChild(textDiv); |
||
102 | var transform; |
||
103 | if (textDiv.dataset.canvasWidth !== undefined) { |
||
104 | // Dataset values come of type string. |
||
105 | var textScale = textDiv.dataset.canvasWidth / width; |
||
106 | transform = 'scaleX(' + textScale + ')'; |
||
107 | } else { |
||
108 | transform = ''; |
||
109 | } |
||
110 | var rotation = textDiv.dataset.angle; |
||
111 | if (rotation) { |
||
112 | transform = 'rotate(' + rotation + 'deg) ' + transform; |
||
113 | } |
||
114 | if (transform) { |
||
115 | CustomStyle.setProp('transform' , textDiv, transform); |
||
116 | } |
||
117 | } |
||
118 | } |
||
119 | |||
120 | this.textLayerDiv.appendChild(textLayerFrag); |
||
121 | this._finishRendering(); |
||
122 | this.updateMatches(); |
||
123 | }, |
||
124 | |||
125 | /** |
||
126 | * Renders the text layer. |
||
127 | * @param {number} timeout (optional) if specified, the rendering waits |
||
128 | * for specified amount of ms. |
||
129 | */ |
||
130 | render: function TextLayerBuilder_render(timeout) { |
||
131 | if (!this.divContentDone || this.renderingDone) { |
||
132 | return; |
||
133 | } |
||
134 | |||
135 | if (this.renderTimer) { |
||
136 | clearTimeout(this.renderTimer); |
||
137 | this.renderTimer = null; |
||
138 | } |
||
139 | |||
140 | if (!timeout) { // Render right away |
||
141 | this.renderLayer(); |
||
142 | } else { // Schedule |
||
143 | var self = this; |
||
144 | this.renderTimer = setTimeout(function() { |
||
145 | self.renderLayer(); |
||
146 | self.renderTimer = null; |
||
147 | }, timeout); |
||
148 | } |
||
149 | }, |
||
150 | |||
151 | appendText: function TextLayerBuilder_appendText(geom, styles) { |
||
152 | var style = styles[geom.fontName]; |
||
153 | var textDiv = document.createElement('div'); |
||
154 | this.textDivs.push(textDiv); |
||
155 | if (isAllWhitespace(geom.str)) { |
||
156 | textDiv.dataset.isWhitespace = true; |
||
157 | return; |
||
158 | } |
||
159 | var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform); |
||
160 | var angle = Math.atan2(tx[1], tx[0]); |
||
161 | if (style.vertical) { |
||
162 | angle += Math.PI / 2; |
||
163 | } |
||
164 | var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])); |
||
165 | var fontAscent = fontHeight; |
||
166 | if (style.ascent) { |
||
167 | fontAscent = style.ascent * fontAscent; |
||
168 | } else if (style.descent) { |
||
169 | fontAscent = (1 + style.descent) * fontAscent; |
||
170 | } |
||
171 | |||
172 | var left; |
||
173 | var top; |
||
174 | if (angle === 0) { |
||
175 | left = tx[4]; |
||
176 | top = tx[5] - fontAscent; |
||
177 | } else { |
||
178 | left = tx[4] + (fontAscent * Math.sin(angle)); |
||
179 | top = tx[5] - (fontAscent * Math.cos(angle)); |
||
180 | } |
||
181 | textDiv.style.left = left + 'px'; |
||
182 | textDiv.style.top = top + 'px'; |
||
183 | textDiv.style.fontSize = fontHeight + 'px'; |
||
184 | textDiv.style.fontFamily = style.fontFamily; |
||
185 | |||
186 | textDiv.textContent = geom.str; |
||
187 | // |fontName| is only used by the Font Inspector. This test will succeed |
||
188 | // when e.g. the Font Inspector is off but the Stepper is on, but it's |
||
189 | // not worth the effort to do a more accurate test. |
||
190 | if (PDFJS.pdfBug) { |
||
191 | textDiv.dataset.fontName = geom.fontName; |
||
192 | } |
||
193 | // Storing into dataset will convert number into string. |
||
194 | if (angle !== 0) { |
||
195 | textDiv.dataset.angle = angle * (180 / Math.PI); |
||
196 | } |
||
197 | // We don't bother scaling single-char text divs, because it has very |
||
198 | // little effect on text highlighting. This makes scrolling on docs with |
||
199 | // lots of such divs a lot faster. |
||
200 | if (textDiv.textContent.length > 1) { |
||
201 | if (style.vertical) { |
||
202 | textDiv.dataset.canvasWidth = geom.height * this.viewport.scale; |
||
203 | } else { |
||
204 | textDiv.dataset.canvasWidth = geom.width * this.viewport.scale; |
||
205 | } |
||
206 | } |
||
207 | }, |
||
208 | |||
209 | setTextContent: function TextLayerBuilder_setTextContent(textContent) { |
||
210 | this.textContent = textContent; |
||
211 | |||
212 | var textItems = textContent.items; |
||
213 | for (var i = 0, len = textItems.length; i < len; i++) { |
||
214 | this.appendText(textItems[i], textContent.styles); |
||
215 | } |
||
216 | this.divContentDone = true; |
||
217 | }, |
||
218 | |||
219 | convertMatches: function TextLayerBuilder_convertMatches(matches) { |
||
220 | var i = 0; |
||
221 | var iIndex = 0; |
||
222 | var bidiTexts = this.textContent.items; |
||
223 | var end = bidiTexts.length - 1; |
||
224 | var queryLen = (this.findController === null ? |
||
225 | 0 : this.findController.state.query.length); |
||
226 | var ret = []; |
||
227 | |||
228 | for (var m = 0, len = matches.length; m < len; m++) { |
||
229 | // Calculate the start position. |
||
230 | var matchIdx = matches[m]; |
||
231 | |||
232 | // Loop over the divIdxs. |
||
233 | while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) { |
||
234 | iIndex += bidiTexts[i].str.length; |
||
235 | i++; |
||
236 | } |
||
237 | |||
238 | if (i === bidiTexts.length) { |
||
239 | console.error('Could not find a matching mapping'); |
||
240 | } |
||
241 | |||
242 | var match = { |
||
243 | begin: { |
||
244 | divIdx: i, |
||
245 | offset: matchIdx - iIndex |
||
246 | } |
||
247 | }; |
||
248 | |||
249 | // Calculate the end position. |
||
250 | matchIdx += queryLen; |
||
251 | |||
252 | // Somewhat the same array as above, but use > instead of >= to get |
||
253 | // the end position right. |
||
254 | while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) { |
||
255 | iIndex += bidiTexts[i].str.length; |
||
256 | i++; |
||
257 | } |
||
258 | |||
259 | match.end = { |
||
260 | divIdx: i, |
||
261 | offset: matchIdx - iIndex |
||
262 | }; |
||
263 | ret.push(match); |
||
264 | } |
||
265 | |||
266 | return ret; |
||
267 | }, |
||
268 | |||
269 | renderMatches: function TextLayerBuilder_renderMatches(matches) { |
||
270 | // Early exit if there is nothing to render. |
||
271 | if (matches.length === 0) { |
||
272 | return; |
||
273 | } |
||
274 | |||
275 | var bidiTexts = this.textContent.items; |
||
276 | var textDivs = this.textDivs; |
||
277 | var prevEnd = null; |
||
278 | var pageIdx = this.pageIdx; |
||
279 | var isSelectedPage = (this.findController === null ? |
||
280 | false : (pageIdx === this.findController.selected.pageIdx)); |
||
281 | var selectedMatchIdx = (this.findController === null ? |
||
282 | -1 : this.findController.selected.matchIdx); |
||
283 | var highlightAll = (this.findController === null ? |
||
284 | false : this.findController.state.highlightAll); |
||
285 | var infinity = { |
||
286 | divIdx: -1, |
||
287 | offset: undefined |
||
288 | }; |
||
289 | |||
290 | function beginText(begin, className) { |
||
291 | var divIdx = begin.divIdx; |
||
292 | textDivs[divIdx].textContent = ''; |
||
293 | appendTextToDiv(divIdx, 0, begin.offset, className); |
||
294 | } |
||
295 | |||
296 | function appendTextToDiv(divIdx, fromOffset, toOffset, className) { |
||
297 | var div = textDivs[divIdx]; |
||
298 | var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset); |
||
299 | var node = document.createTextNode(content); |
||
300 | if (className) { |
||
301 | var span = document.createElement('span'); |
||
302 | span.className = className; |
||
303 | span.appendChild(node); |
||
304 | div.appendChild(span); |
||
305 | return; |
||
306 | } |
||
307 | div.appendChild(node); |
||
308 | } |
||
309 | |||
310 | var i0 = selectedMatchIdx, i1 = i0 + 1; |
||
311 | if (highlightAll) { |
||
312 | i0 = 0; |
||
313 | i1 = matches.length; |
||
314 | } else if (!isSelectedPage) { |
||
315 | // Not highlighting all and this isn't the selected page, so do nothing. |
||
316 | return; |
||
317 | } |
||
318 | |||
319 | for (var i = i0; i < i1; i++) { |
||
320 | var match = matches[i]; |
||
321 | var begin = match.begin; |
||
322 | var end = match.end; |
||
323 | var isSelected = (isSelectedPage && i === selectedMatchIdx); |
||
324 | var highlightSuffix = (isSelected ? ' selected' : ''); |
||
325 | |||
326 | if (this.findController) { |
||
327 | this.findController.updateMatchPosition(pageIdx, i, textDivs, |
||
328 | begin.divIdx, end.divIdx); |
||
329 | } |
||
330 | |||
331 | // Match inside new div. |
||
332 | if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { |
||
333 | // If there was a previous div, then add the text at the end. |
||
334 | if (prevEnd !== null) { |
||
335 | appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); |
||
336 | } |
||
337 | // Clear the divs and set the content until the starting point. |
||
338 | beginText(begin); |
||
339 | } else { |
||
340 | appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset); |
||
341 | } |
||
342 | |||
343 | if (begin.divIdx === end.divIdx) { |
||
344 | appendTextToDiv(begin.divIdx, begin.offset, end.offset, |
||
345 | 'highlight' + highlightSuffix); |
||
346 | } else { |
||
347 | appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, |
||
348 | 'highlight begin' + highlightSuffix); |
||
349 | for (var n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) { |
||
350 | textDivs[n0].className = 'highlight middle' + highlightSuffix; |
||
351 | } |
||
352 | beginText(end, 'highlight end' + highlightSuffix); |
||
353 | } |
||
354 | prevEnd = end; |
||
355 | } |
||
356 | |||
357 | if (prevEnd) { |
||
358 | appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); |
||
359 | } |
||
360 | }, |
||
361 | |||
362 | updateMatches: function TextLayerBuilder_updateMatches() { |
||
363 | // Only show matches when all rendering is done. |
||
364 | if (!this.renderingDone) { |
||
365 | return; |
||
366 | } |
||
367 | |||
368 | // Clear all matches. |
||
369 | var matches = this.matches; |
||
370 | var textDivs = this.textDivs; |
||
371 | var bidiTexts = this.textContent.items; |
||
372 | var clearedUntilDivIdx = -1; |
||
373 | |||
374 | // Clear all current matches. |
||
375 | for (var i = 0, len = matches.length; i < len; i++) { |
||
376 | var match = matches[i]; |
||
377 | var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); |
||
378 | for (var n = begin, end = match.end.divIdx; n <= end; n++) { |
||
379 | var div = textDivs[n]; |
||
380 | div.textContent = bidiTexts[n].str; |
||
381 | div.className = ''; |
||
382 | } |
||
383 | clearedUntilDivIdx = match.end.divIdx + 1; |
||
384 | } |
||
385 | |||
386 | if (this.findController === null || !this.findController.active) { |
||
387 | return; |
||
388 | } |
||
389 | |||
390 | // Convert the matches on the page controller into the match format |
||
391 | // used for the textLayer. |
||
392 | this.matches = this.convertMatches(this.findController === null ? |
||
393 | [] : (this.findController.pageMatches[this.pageIdx] || [])); |
||
394 | this.renderMatches(this.matches); |
||
395 | } |
||
396 | }; |
||
397 | return TextLayerBuilder; |
||
398 | })(); |
||
399 | |||
420 |